
from django.db.models.base import ModelBase
from django.db.models.query import QuerySet
from django.db.models import Avg, Max, Min, Count, Q
import operator, copy

from fb_utils.utils import *

from djfacet.constants import *


##################
#  Wed Aug	4 17:32:49 BST 2010
# QueryHelper:
#
##################





class QueryHelper(object):
	""" Contains all the methods needed for updating the FacetValues Count
	all_values_flag: added so to try to speed up the query time.. but it didnt' have any effect....

	"""

	def __init__(self, resulttypeName, facet, activeIDs, all_values_flag=False):
		self.facet = facet
		self.resulttypeName = resulttypeName
		self.activeIDs = activeIDs
		self.cache = None
		self.all_values_flag = all_values_flag
		# this is called only once..
		self.resulttypeModel = self.get_resulttype_from_name(resulttypeName)['infospace']




	def calculate_facetvalues(self, valuesbatch = None):  
		""" returns a list of FacetValue objects with updated counts
			.. it's a wrapper for the '__calcvalues' method.....
			
			VALUESBATCH: it is used only by cacheDB, *only* for the old algorithm, to avoid memory crashes
			ALL_VALUES :  flag that indicates whether we're passing the entire dataset as activeIDs

		"""
		
		# various cases, depending on whether the reversedBehaviour has been defined manually or not.
		facet , resulttypeName = self.facet , self.resulttypeName
		inverseBehaviour = facet.get_inversebehaviour(resulttypeName)
		djfacetlog("\n+++++\n[***TEST***] REFRESH Facet Values: Facet= *%s*	 Result= *%s* \n+++++" % (str(facet.uniquename), str(resulttypeName)))
		if inverseBehaviour: 
			if type(inverseBehaviour) == type({}):
				if inverseBehaviour.get('GENERIC-FIELD'):
					return self._calcvalues(parameter='GENERIC-FIELD', valuesbatch = valuesbatch)
			elif type(inverseBehaviour) == type(('a tuple',)):
				return self._calcvalues(inverseBehaviour=inverseBehaviour, valuesbatch = valuesbatch)
			elif type(inverseBehaviour) == type("a string"):
				return self._calcvalues(inverseBehaviour=(inverseBehaviour,), valuesbatch = valuesbatch)
		else:
			return self._calcvalues(valuesbatch = valuesbatch)



	def should_we_use_distinct(self, facetModel, resultModel):
		""" Returns false is the two models are different (proxy models don't count)
			We found out that if they are the same the 'Count' query does not need the 'distinct' directive
			.... [2010-10-28]
		 """
		if facetModel._meta.proxy:
			facetModel = facetModel._meta.proxy_for_model
		if resultModel._meta.proxy:
			resultModel = resultModel._meta.proxy_for_model		
			
		if facetModel == resultModel:
			djfacetlog(".... Detected exception: *facetModel == resultModel*  ==> no need to use *distinct* in Count query")
			return False
		return True
		
		

	def _calcvalues(self, parameter = None, inverseBehaviour = None , valuesbatch = None):
		"""Inner method that computes the available facetvalues"""
		# SET UP VARIABLES
		resulttypeName, facet, activeIDs, all_values_flag = self.resulttypeName, self.facet, self.activeIDs, self.all_values_flag
		resulttypeModel = self.resulttypeModel
		dbfield = facet.originalAttribute

		if parameter == "GENERIC-FIELD":
			# in this case, we have an attribute that applies to all models (cf.'revision' or 'edited record' facets in LTB)
			facetModel = resulttypeModel
		else:
			facetModel = facet.originalModel
		# 2010-08-20: COMMENTED OUT WHAT FOLLOWS... will LTB WORK ? ? ? ? NEED TO TEST
		# CAUSE I DONT SEE ANY REASON WHY IT SHOULDN'T....
		# if facetModel._meta.proxy:	# with Proxy models queries seem to break
		#	facetModel = facetModel._meta.proxy_for_model

		# CALCULATE INVERSE BEHAVIOUR, IF NOT PROVIDED
		if inverseBehaviour:
			djfacetlog("Found INVERSEBEHAVIOUR = %s" % str(inverseBehaviour))
			reversed_paths_list=[]
			for path in inverseBehaviour:
				reversed_paths_list.append(path)					
		else:		
			explodedPathList = self.getExplodedPaths()
			djfacetlog("Calculated EXPLODEDPATHLIST = %s" % str(explodedPathList))
			# extract the reversed query_strings...
			# mind that even if explodedPathList is empty, that is =[{}], the iteration runs one time anyways!
			# this is the case when a facet is just a text field on the same model we are querying
			reversed_paths_list=[]
			for expl_path in explodedPathList:
				reversed_paths_list.append(self._calcReversedPath(expl_path))

		djfacetlog("...RUNNING Query USING VALUES:\n**FacetModel: %s\n**ReverseQuerypaths: %s\n**DbField: %s\n**ResultTypeModel :%s" % (facetModel, 
				str(reversed_paths_list), dbfield, str(resulttypeModel)))

		
		# **** in this case we have a sequence of ORs... we didn't find a way to use Count so I apply the old algorithm
		if len(reversed_paths_list) > 1:
			return self._calcvaluesForORQueries(reversed_paths_list, facetModel)
		# **** in this case there are no ORs in the specs definition, so we can use Django's annotate function (=Count)
		else:
			if reversed_paths_list[0]: 
				count_argument = reversed_paths_list[0]
			else: # *** if there's a case with GENERIC FIELDS, the count_argument is the same as dbfield
				count_argument = dbfield
			return self._calcvaluesForStandardQueries(reversed_paths_list[0], count_argument, facetModel)





	def _calcvaluesForORQueries(self, reversed_paths_list, facetModel):
		"""This method can be very time-expensive as OR queries with lots of JOINs take a long time
		   We suggest to avoid OR queries if possible, and if needed create auxiliary tables...
		"""
		resulttypeName, facet, activeIDs, all_values_flag = self.resulttypeName, self.facet, self.activeIDs, self.all_values_flag
		resulttypeModel = self.resulttypeModel
		dbfield = facet.originalAttribute
		if not all_values_flag or resulttypeModel._meta.proxy:	 # if have args (resulttypeModel._meta.proxy means than we can't use the default manager!)
			if resulttypeModel._meta.proxy:
				djfacetlog("resulttypeModel._meta.proxy == True!")
				activeIDs = list(set(activeIDs).intersection(set([x.id for x in resulttypeModel.objects.all()])))	
			args = []
			for path in reversed_paths_list:
				temp = {}
				if path:
					temp[path + "__id__in"] = activeIDs
				else:  # if path is '' or null
					temp["id__in"] = activeIDs
				args.append(Q(**temp))
			or_args = reduce(operator.or_,args)			

			# 1) get all possible valid facetValues, without counting
			#	2010-08-12 => added the Count, cause this way we're sure we have no duplicates!	
			djfacetlog("....starting query calculation......")		
			if facet.ordering:
				valid_values = facetModel.objects.filter(or_args).values(dbfield).annotate(Count(dbfield)).order_by(facet.ordering)
			else:
				valid_values = facetModel.objects.filter(or_args).values(dbfield).annotate(Count(dbfield))			
		else:  # if all_values_flag
			if facet.ordering:
				valid_values = facetModel.objects.values(dbfield).annotate(Count(dbfield)).order_by(facet.ordering)
			else:
				valid_values = facetModel.objects.values(dbfield).annotate(Count(dbfield))			


		if facet.hierarchyoptions:	# TO BE IMPLEMENTED 
			pass   # eg result: [{'count': 1, 'forename': u'Abraham'}, {'count': 1, 'forename': u'Aceard'},etc...]

		# 2) for each value, run the intersection routine to find out the count
		output = []
		counter = 0
		totvalues = len(valid_values)
		djfacetlog("____________ ==> got the possible facet values _____________ tot: %s\nUsing MANUAL COUNT algorithm because we have an OR query....\n" % str(totvalues))
		for value in valid_values:
			counter += 1
			try:  #something this fails due to ascii encoding errors
				djfacetlog("===>> updating FV count for value: *%s* ==> [%d of %d]" % (str(value[dbfield]), counter,  totvalues))
			except:
				pass
			args = []
			# 2.1) construct the query as if we were querying the result (in the facetedManager)
			for behavior in self.getBehaviourTuple():
				temp = {}
				temp[behavior] = value[dbfield] 
				args.append(Q(**temp))
			or_args = reduce(operator.or_,args) 
			# 2.2) do the intersection of activeObjects with objects that have a certain value in facet
			#	   Count them, this is the number we're looking for
			if not all_values_flag:	 # if have args
				number = resulttypeModel.objects.filter(id__in=activeIDs).filter(or_args).distinct().count()
			else:
				number = resulttypeModel.objects.filter(or_args).distinct().count()
			# djfacetlog('noise')	
			# djfacetlog(number)	
				
			# 3) find the facetValue object, and update the count
			if number:
				fv = facet.get_facetvalue_from_name(value[dbfield])
				if fv:
					fv_copy = copy.copy(fv)	 #make a copy, so we don't update the static FM object
					fv_copy.howmany = number
					output.append(fv_copy)
		if facet.exclude:  # this is just to flag it up: the real exclusion happens at FM loading time (see facet.py)
			djfacetlog("=====>> ****** excluding values found in facet.exclude: %s" % str(facet.exclude))
		return output




	def _calcvaluesForStandardQueries(self, reversed_path, count_argument, facetModel):
		resulttypeName, facet, activeIDs, all_values_flag = self.resulttypeName, self.facet, self.activeIDs, self.all_values_flag
		resulttypeModel = self.resulttypeModel
		dbfield = facet.originalAttribute
		# if have args, or the args are hidden via a proxy
		if not all_values_flag or resulttypeModel._meta.proxy:	 
			if resulttypeModel._meta.proxy:
				djfacetlog("... you're using a PROXY MODEL as a results type  ==> I'm adding that implicit behaviour to the query......")
				activeIDs = list(set(activeIDs).intersection(set([x.id for x in resulttypeModel.objects.all()])))
			temp = {}
			if reversed_path:
				temp[reversed_path + "__id__in"] = activeIDs
			else:  # if path is '' or null
				temp["id__in"] = activeIDs
			# eg: Gender.objects.filter(person__id__in=range(1000, 2000)).values('name').annotate(Count('person', distinct=True))
			if facet.ordering:
				if self.should_we_use_distinct(facetModel, resulttypeModel):
					valid_values = facetModel.objects.filter(**temp).values(dbfield).annotate(count=Count(count_argument, distinct=True)).order_by(facet.ordering)				
				else:
					valid_values = facetModel.objects.filter(**temp).values(dbfield).annotate(count=Count(count_argument)).order_by(facet.ordering)
			else:
				if self.should_we_use_distinct(facetModel, resulttypeModel):
					valid_values = facetModel.objects.filter(**temp).values(dbfield).annotate(count=Count(count_argument, distinct=True))	 #.order_by(facet.ordering)				
				else:
					valid_values = facetModel.objects.filter(**temp).values(dbfield).annotate(count=Count(count_argument))	 #.order_by(facet.ordering)				
		# in this case we have no args
		else:
			if facet.ordering:
				if self.should_we_use_distinct(facetModel, resulttypeModel):
					valid_values = facetModel.objects.values(dbfield).annotate(count=Count(count_argument, distinct=True)).order_by(facet.ordering)
				else:
					valid_values = facetModel.objects.values(dbfield).annotate(count=Count(count_argument)).order_by(facet.ordering)
			else:	
				if self.should_we_use_distinct(facetModel, resulttypeModel):		
					valid_values = facetModel.objects.values(dbfield).annotate(count=Count(count_argument, distinct=True))	 #.order_by(facet.ordering)
				else:
					valid_values = facetModel.objects.values(dbfield).annotate(count=Count(count_argument))	 #.order_by(facet.ordering)

		# eg result: [{'count': 1, 'forename': u'Abraham'}, {'count': 1, 'forename': u'Aceard'},etc...]
		# -----------------------------------------

		if facet.hierarchyoptions:	
			if facet.hierarchyoptions.get('alpha'):
				pass   	#	THIS BIT IS NOT IMPLEMENTED YET
			# if facet.hierarchyoptions.get('alpha'):
			#	valid_values = facetModel.objects.filter(or_args).values(dbfield)
			#	# extract initials, remove duplicates, and make everything uppercase
			#	lookup_choices = list(set([val[dbfield][0].upper() for val in valid_values if val[dbfield]]))
			#	# now just a trick to reformat the list of dicts as in the other cases....
			#	valid_values = [{dbfield : x} for x in lookup_choices]

			if facet.hierarchyoptions.get('range'):
				pass  #	NOT IMPLEMENTED YET
				# eg from 30, we find min and max available and create [1900-1930, 1930-1960, etc.]
				# therange = facet.hierarchyoptions['range']
				# values_list = sorted([val[dbfield] for val in valid_values if val[dbfield]])
				# if values_list[0] == 0:
				# 	values_list.remove(0)
				# themin = findmin(therange, min(values_list))
				# themax = findmax(therange, max(values_list))
				# rangelist = buildranges(themin, themax, therange) #[('990-1020', (990, 1020))]
				# for x in rangelist:
				# 	v =	 FacetValue(name=x[0], facet=self, hierarchytype='range', hierarchyextra=x[1], 
				# 						mask=self.mask)
				# 	self.add_facetvalue(v)


		# for each value, UPDATE THE FV OBJECTS
		counter = 0
		totvalues = len(valid_values)
		djfacetlog("____________ ==> got the possible facet values _____________ tot: %s\n" % str(totvalues))
		output = []
		for value in valid_values:
			counter += 1
			try:  #something this fails due to ascii encoding errors
				djfacetlog("===>> updating FV count for value: *%s* ==> +%d+	[%d of %d]" % (str(value[dbfield]), value['count'], counter,  totvalues))
			except:
				pass

			if value['count']: # some object's count is Zero
				fv = facet.get_facetvalue_from_name(value[dbfield])
				if fv:
					fv_copy = copy.copy(fv)	 #make a copy, so we don't update the static FM object
					fv_copy.howmany = value['count']
					output.append(fv_copy)
		if facet.exclude:  # this is just to flag it up: the real exclusion happens at FM loading time (see facet.py)
			djfacetlog("=====>> ****** Mind that we're excluding values found in facet.exclude: %s" % str(facet.exclude))
		return output









# ========
# HELPER METHODS
# ========
	

	def _calcReversedPath(self, explodedPath):
		# PLEASE explain the line below - otherwise you'll forget! ++++
		newpath = "__".join([explodedPath[k]['inversePath'] for k in sorted(explodedPath.iterkeys(), reverse=True) if explodedPath[k]['toModel'] is not None])
				# newpath = newpath + "__id__in"				
				# case when startmodel and endmodel are the same, 
				# eg a facet is just a text field on the same model we are querying (= the result type)
				# if newpath == "__id__in":
				#	newpath = "id__in"
		return newpath

	

	def getExplodedPaths(self):
		""" From the current instance of QueryHelper(resType, Facet, ActiveIds)
		 ==> returns the whole exploded path(s) based on the behaviours of the Facet for that resType. 
		The result is a list of explodedPaths..

		EG of exploded queryPath:
		[{1: {'path': 'record', 'inversePath': 'event', 'type': 'RelatedObject', 
			'toModel': <class 'ltb.ltbapp.models.Record'>, 'fromModel': <class 'ltb.ltbapp.models.Event'>}, 
		2: {'path': 'transcription', 'inversePath': 'transcr_inrec', 'type': 'ForeignKey', 
			'toModel': <class 'ltb.ltbapp.models.Document'>, 'fromModel': <class 'ltb.ltbapp.models.Record'>}, 
		3: {'path': 'doc_type', 'inversePath': 'documents', 'type': 'ManyToManyField', 
			'toModel': <class 'ltb.ltbapp.models_authlists.DocumentType'>, 'fromModel': <class 'ltb.ltbapp.models.Document'>}}, 
		{..another path..}]

		"""
		resulttypeModel = self.resulttypeModel
		paths_tuple = self.getBehaviourTuple()
		djfacetlog("ORIGINALBEHAVIOUR = %s" % str(paths_tuple))
		alist = []

		if paths_tuple:
			for path in paths_tuple:
				alist.append((self.inspectQuery(resulttypeModel, path)))
			return alist



	def get_resulttype_from_name(self, name):
		"""Duplicate method in FacetedManager: we put it also here so that QueryHelper is autonomous 
		......get the result_type dictionary from the name """
		# from SPECS import result_types as all_result_types
		for i in DJ_SPECS.result_types:
			if i['uniquename'] == name:
				return i
		return None

	def getBehaviourTuple(self):
		""" extract the behaviour(s) of a facet (given a specific resultType), and puts them into a nice tuple. 
		In the case of multuple behaviours, that is because we have OR queries.. """

		resulttypeName, facet = self.resulttypeName, self.facet
		if not facet.behaviour:
			return None

		paths_tuple = None			
		existing_path = facet.get_behaviour(resulttypeName)
		if existing_path:
			# this is the case with one statement (which becomes an AND in the aggregated query)
			if type(existing_path) == type("a string"):
				paths_tuple = (existing_path,)
			# this is the case with OR statements
			elif type(existing_path) == type(('a tuple',)):					
				paths_tuple = existing_path 
			return paths_tuple

	
	def inspectFieldInfo(self, aModel, aFieldName):
		""" Helper method: Tells you information of a model's field. 
			Particularly useful for KEYS: shows how two Django models are connected..	 
			Given a model, and the field name represeting its KEY, it returns a dict,  for example:		
			{'path': 'record', 'inversePath': 'event', 'type': 'RelatedObject', 
				'toModel': <class 'ltb.ltbapp.models.Record'>, 'fromModel': <class 'ltb.ltbapp.models.Event'>}
		"""

		from django.db.models.fields import FieldDoesNotExist
		try:
			field = aModel._meta.get_field_by_name(aFieldName)[0]
		except FieldDoesNotExist:
			raise Exception, "Field not existing : [%s]" % aFieldName

		if type(field).__name__ == 'RelatedObject':
			return {'fromModel' : aModel, 'path': aFieldName, 'toModel' : field.model, 'inversePath' : field.field.name, 'type' :  type(field).__name__ }
		elif type(field).__name__ == 'ForeignKey':
			return {'fromModel' : aModel, 'path': aFieldName, 'toModel' : field.related.parent_model, 'inversePath' : field.related_query_name(), 'type' :	type(field).__name__ }
			# return [aModel, aFieldName, field.related_query_name(), field.related.parent_model, type(field).__name__ , field]
		elif type(field).__name__ == 'ManyToManyField':
			return {'fromModel' : aModel, 'path': aFieldName, 'toModel' : field.related.parent_model, 'inversePath' : field.related_query_name(), 'type' :	type(field).__name__ }
			# return [aModel, aFieldName, field.related_query_name(), field.related.parent_model, type(field).__name__ , field]
		else:  # NORMAL FIELD: recognizable by the fact that toMolde = None!
			return {'fromModel' : aModel, 'path': aFieldName, 'toModel' : None, 'inversePath' : None, 'type' :	type(field).__name__ }			
			# djfacetlog("inspectFieldInfo: [[%s] %s] is not a KEY field, but a %s" % (str(aModel), str(aFieldName), field.get_internal_type()))
			# return None

	def inspectQuery(self, startmodel, querystring):
		""" Helper method:
			From a model and a query-path, by using the 'inspectFieldInfo' above, we return a list of 
			model objects and methods that break down all the query, EG:

			>>> q.inspectQuery(Event, 'record__transcription__doc_type__name')
			{1: {'path': 'record', 'inversePath': 'event', 'type': 'RelatedObject', 
				'toModel': <class 'ltb.ltbapp.models.Record'>, 'fromModel': <class 'ltb.ltbapp.models.Event'>}, 
			2: {'path': 'transcription', 'inversePath': 'transcr_inrec', 'type': 'ForeignKey', 
				'toModel': <class 'ltb.ltbapp.models.Document'>, 'fromModel': <class 'ltb.ltbapp.models.Record'>}, 
			3: {'path': 'doc_type', 'inversePath': 'documents', 'type': 'ManyToManyField', 
				'toModel': <class 'ltb.ltbapp.models_authlists.DocumentType'>, 'fromModel': <class 'ltb.ltbapp.models.Document'>}}

				+++ add that we include info about the latest field too!!! ++++

		"""
		exit = {}
		n = 0	# to order the result
		tempmodel = startmodel
		elements = querystring.split("__")

		for el in elements:
			if tempmodel:
				# djfacetlog("el: %s, model: %s" % (el, tempmodel))
				res = self.inspectFieldInfo(tempmodel, el)
				if res:	 # = if the field represents a KEY
					n += 1
					exit[n] = res
					tempmodel = res['toModel']
				else:
					break	# CHANGE: meaning that if the querystring doesn't produce KEYS, we return nothing
		return exit



